package net.nightwhistler.htmlspanner.css;
import android.graphics.Color;
import android.util.Log;
import com.osbcp.cssparser.PropertyValue;
import com.osbcp.cssparser.Rule;
import com.osbcp.cssparser.Selector;
import net.nightwhistler.htmlspanner.FontFamily;
import net.nightwhistler.htmlspanner.HtmlSpanner;
import net.nightwhistler.htmlspanner.style.Style;
import net.nightwhistler.htmlspanner.style.StyleValue;
import org.htmlcleaner.TagNode;
import java.util.ArrayList;
import java.util.List;
/**
* Compiler for CSS Rules.
*
* The compiler takes the raw parsed form (a Rule) of a CSS rule
* and transforms it into an executable CompiledRule where all
* the parsing of values has already been done.
*
*
*/
public class CSSCompiler {
public static interface StyleUpdater {
Style updateStyle( Style style, HtmlSpanner spanner );
}
public static interface TagNodeMatcher {
boolean matches( TagNode tagNode );
}
public static CompiledRule compile( Rule rule, HtmlSpanner spanner ) {
Log.d("CSSCompiler", "Compiling rule " + rule );
List<List<TagNodeMatcher>> matchers = new ArrayList<List<TagNodeMatcher>>();
List<StyleUpdater> styleUpdaters = new ArrayList<StyleUpdater>();
for ( Selector selector: rule.getSelectors() ) {
List<CSSCompiler.TagNodeMatcher> selMatchers = CSSCompiler.createMatchersFromSelector(selector);
matchers.add( selMatchers );
}
Style blank = new Style();
for ( PropertyValue propertyValue: rule.getPropertyValues() ) {
CSSCompiler.StyleUpdater updater = CSSCompiler.getStyleUpdater(propertyValue.getProperty(),
propertyValue.getValue());
if ( updater != null ) {
styleUpdaters.add( updater );
blank = updater.updateStyle(blank, spanner);
}
}
Log.d("CSSCompiler", "Compiled rule: " + blank );
String asText = rule.toString();
return new CompiledRule(spanner, matchers, styleUpdaters, asText );
}
public static Integer parseCSSColor( String colorString ) {
//Check for CSS short-hand notation: #0fc -> #00ffcc
if ( colorString.length() == 4 && colorString.startsWith("#") ) {
StringBuilder builder = new StringBuilder("#");
for ( int i =1; i < colorString.length(); i++ ) {
//Duplicate each char
builder.append( colorString.charAt(i) );
builder.append( colorString.charAt(i) );
}
colorString = builder.toString();
}
return Color.parseColor(colorString);
}
public static List<TagNodeMatcher> createMatchersFromSelector( Selector selector ) {
List<TagNodeMatcher> matchers = new ArrayList<TagNodeMatcher>();
String selectorString = selector.toString();
String[] parts = selectorString.split("\\s");
//Create a reversed matcher list
for ( int i=parts.length -1; i >= 0; i-- ) {
matchers.add( createMatcherFromPart(parts[i]));
}
return matchers;
}
private static TagNodeMatcher createMatcherFromPart( String selectorPart ) {
//Match by class
if ( selectorPart.indexOf('.') != -1 ) {
return new ClassMatcher(selectorPart);
}
if ( selectorPart.startsWith("#") ) {
return new IdMatcher( selectorPart );
}
return new TagNameMatcher(selectorPart);
}
private static class ClassMatcher implements TagNodeMatcher {
private String tagName;
private String className;
private ClassMatcher( String selectorString ) {
String[] elements = selectorString.split("\\.");
if ( elements.length == 2 ) {
tagName = elements[0];
className = elements[1];
}
}
@Override
public boolean matches(TagNode tagNode) {
if ( tagNode == null ) {
return false;
}
//If a tag name is given it should match
if (tagName != null && tagName.length() > 0 && ! tagName.equals(tagNode.getName() ) ) {
return false;
}
String classAttribute = tagNode.getAttributeByName("class");
return classAttribute != null && classAttribute.equals(className);
}
}
private static class TagNameMatcher implements TagNodeMatcher {
private String tagName;
private TagNameMatcher( String selectorString ) {
this.tagName = selectorString.trim();
}
@Override
public boolean matches(TagNode tagNode) {
return tagNode != null && tagName.equalsIgnoreCase( tagNode.getName() );
}
}
private static class IdMatcher implements TagNodeMatcher {
private String id;
private IdMatcher( String selectorString ) {
id = selectorString.substring(1);
}
@Override
public boolean matches(TagNode tagNode) {
if ( tagNode == null ) {
return false;
}
String idAttribute = tagNode.getAttributeByName("id");
return idAttribute != null && idAttribute.equals( id );
}
}
public static StyleUpdater getStyleUpdater( final String key, final String value) {
if ( "color".equals(key)) {
try {
final Integer color = parseCSSColor(value);
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Log.d("CSSCompiler", "Applying style " + key + ": " + value );
return style.setColor(color);
}
};
} catch ( IllegalArgumentException ia ) {
Log.e("CSSCompiler", "Can't parse colour definition: " + value);
return null;
}
}
if ( "background-color".equals(key) ) {
try {
final Integer color = parseCSSColor(value);
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Log.d("CSSCompiler", "Applying style " + key + ": " + value );
return style.setBackgroundColor(color);
}
};
} catch ( IllegalArgumentException ia ) {
Log.e("CSSCompiler", "Can't parse colour definition: " + value);
return null;
}
}
if ( "align".equals(key) || "text-align".equals(key)) {
try {
final Style.TextAlignment alignment = Style.TextAlignment.valueOf(value.toUpperCase());
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Log.d("CSSCompiler", "Applying style " + key + ": " + value );
return style.setTextAlignment(alignment);
}
};
} catch ( IllegalArgumentException i ) {
Log.e("CSSCompiler", "Can't parse alignment: " + value);
return null;
}
}
if ( "font-weight".equals(key)) {
try {
final Style.FontWeight weight = Style.FontWeight.valueOf(value.toUpperCase());
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Log.d("CSSCompiler", "Applying style " + key + ": " + value );
return style.setFontWeight(weight);
}
};
} catch ( IllegalArgumentException i ) {
Log.e("CSSCompiler", "Can't parse font-weight: " + value);
return null;
}
}
if ( "font-style".equals(key)) {
try {
final Style.FontStyle fontStyle = Style.FontStyle.valueOf(value.toUpperCase());
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Log.d("CSSCompiler", "Applying style " + key + ": " + value );
return style.setFontStyle(fontStyle);
}
};
}
catch ( IllegalArgumentException i ) {
Log.e("CSSCompiler", "Can't parse font-style: " + value);
return null;
}
}
if ( "font-family".equals(key)) {
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Log.d("CSSCompiler", "Applying style " + key + ": " + value );
FontFamily family = spanner.getFont( value );
Log.d("CSSCompiler", "Got font " + family );
return style.setFontFamily(family);
}
};
}
if ( "font-size".equals(key)) {
final StyleValue styleValue = StyleValue.parse( value );
if ( styleValue != null ) {
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Log.d("CSSCompiler", "Applying style " + key + ": " + value );
return style.setFontSize(styleValue);
}
};
} else {
//Fonts have an extra legacy format where you just specify a plain number.
try {
final Float number = translateFontSize(Integer.parseInt(value));
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Log.d("CSSCompiler", "Applying style " + key + ": " + value );
return style.setFontSize(new StyleValue(number, StyleValue.Unit.EM));
}
};
} catch ( NumberFormatException nfe ) {
Log.e("CSSCompiler", "Can't parse font-size: " + value );
return null;
}
}
}
if ( "margin-bottom".equals(key) ) {
final StyleValue styleValue = StyleValue.parse( value );
if ( styleValue != null ) {
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setMarginBottom(styleValue);
}
};
}
}
if ( "margin-top".equals(key) ) {
final StyleValue styleValue = StyleValue.parse( value );
if ( styleValue != null ) {
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setMarginTop(styleValue);
}
};
}
}
if ( "margin-left".equals(key) ) {
final StyleValue styleValue = StyleValue.parse( value );
if ( styleValue != null ) {
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setMarginLeft(styleValue);
}
};
}
}
if ( "margin-right".equals(key) ) {
final StyleValue styleValue = StyleValue.parse( value );
if ( styleValue != null ) {
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setMarginRight(styleValue);
}
};
}
}
if ( "margin".equals( key ) ) {
return parseMargin( value );
}
if ( "text-indent".equals(key) ) {
final StyleValue styleValue = StyleValue.parse( value );
if ( styleValue != null ) {
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setTextIndent(styleValue);
}
};
}
}
if ( "display".equals( key ) ) {
try {
final Style.DisplayStyle displayStyle = Style.DisplayStyle.valueOf( value.toUpperCase() );
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setDisplayStyle(displayStyle);
}
};
} catch (IllegalArgumentException ia) {
Log.e("CSSCompiler", "Can't parse display-value: " + value );
return null;
}
}
if ( "border-style".equals( key ) ) {
try {
final Style.BorderStyle borderStyle = Style.BorderStyle.valueOf(value.toUpperCase());
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setBorderStyle( borderStyle );
}
};
} catch (IllegalArgumentException ia) {
Log.e("CSSCompiler", "Could not parse border-style " + value );
return null;
}
}
if ( "border-color".equals( key ) ) {
try {
final Integer borderColor = parseCSSColor(value);
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setBorderColor( borderColor );
}
};
} catch (IllegalArgumentException ia) {
Log.e("CSSCompiler", "Could not parse border-color " + value );
return null;
}
}
if ( "border-width".equals( key ) ) {
final StyleValue borderWidth = StyleValue.parse(value);
if ( borderWidth != null ) {
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
return style.setBorderWidth( borderWidth );
}
};
} else {
Log.e("CSSCompiler", "Could not parse border-color " + value );
return null;
}
}
if ( "border".equals( key ) ) {
return parseBorder( value );
}
Log.d("CSSCompiler", "Don't understand CSS property '" + key + "'. Ignoring it.");
return null;
}
private static float translateFontSize( int fontSize ) {
switch (fontSize ) {
case 1:
return 0.6f;
case 2:
return 0.8f;
case 3:
return 1.0f;
case 4:
return 1.2f;
case 5:
return 1.4f;
case 6:
return 1.6f;
case 7:
return 1.8f;
}
return 1.0f;
}
/**
* Parses a border definition.
*
* Border definitions are a complete mess, since the order is not set.
*
* @param borderDefinition
* @return
*/
private static StyleUpdater parseBorder( String borderDefinition ) {
String[] parts = borderDefinition.split("\\s");
StyleValue borderWidth = null;
Integer borderColor = null;
Style.BorderStyle borderStyle = null;
for ( String part: parts ) {
Log.d("CSSParser", "Trying to parse " + part );
if ( borderWidth == null ) {
borderWidth = StyleValue.parse( part );
if ( borderWidth != null ) {
Log.d("CSSParser", "Parsed " + part + " as border-width");
continue;
}
}
if ( borderColor == null ) {
try {
borderColor = parseCSSColor(part);
Log.d("CSSParser", "Parsed " + part + " as border-color");
continue;
} catch ( IllegalArgumentException ia ) {
//try next one
}
}
if ( borderStyle == null ) {
try {
borderStyle = Style.BorderStyle.valueOf(part.toUpperCase());
Log.d("CSSParser", "Parsed " + part + " as border-style");
continue;
} catch ( IllegalArgumentException ia ) {
//next loop iteration
}
}
Log.d("CSSParser", "Could not make sense of border-spec " + part );
}
final StyleValue finalBorderWidth = borderWidth;
final Integer finalBorderColor = borderColor;
final Style.BorderStyle finalBorderStyle = borderStyle;
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
if ( finalBorderColor != null ) {
style = style.setBorderColor(finalBorderColor);
}
if ( finalBorderWidth != null ) {
style = style.setBorderWidth( finalBorderWidth );
}
if ( finalBorderStyle != null ) {
style = style.setBorderStyle( finalBorderStyle );
}
return style;
}
};
}
private static StyleUpdater parseMargin( String marginValue ) {
String[] parts = marginValue.split("\\s");
String bottomMarginString = "";
String topMarginString = "";
String leftMarginString = "";
String rightMarginString = "";
//See http://www.w3schools.com/css/css_margin.asp
if ( parts.length == 1 ) {
bottomMarginString = parts[0];
topMarginString = parts[0];
leftMarginString = parts[0];
rightMarginString = parts[0];
} else if ( parts.length == 2 ) {
topMarginString = parts[0];
bottomMarginString = parts[0];
leftMarginString = parts[1];
rightMarginString = parts[1];
} else if ( parts.length == 3 ) {
topMarginString = parts[0];
leftMarginString = parts[1];
rightMarginString = parts[1];
bottomMarginString = parts[2];
} else if ( parts.length == 4 ) {
topMarginString = parts[0];
rightMarginString = parts[1];
bottomMarginString = parts[2];
leftMarginString = parts[3];
}
final StyleValue marginBottom = StyleValue.parse( bottomMarginString );
final StyleValue marginTop = StyleValue.parse( topMarginString );
final StyleValue marginLeft = StyleValue.parse( leftMarginString );
final StyleValue marginRight = StyleValue.parse( rightMarginString );
return new StyleUpdater() {
@Override
public Style updateStyle(Style style, HtmlSpanner spanner) {
Style resultStyle = style;
if ( marginBottom != null ) {
resultStyle = resultStyle.setMarginBottom(marginBottom);
}
if ( marginTop != null ) {
resultStyle = resultStyle.setMarginTop(marginTop);
}
if ( marginLeft != null ) {
resultStyle = resultStyle.setMarginLeft(marginLeft);
}
if ( marginRight != null ) {
resultStyle = resultStyle.setMarginRight(marginRight);
}
return resultStyle;
}
};
}
}